Skip to content

Floor DateTime truncation for pre-epoch timestamps - #3002

Open
winklemad wants to merge 3 commits into
quickwit-oss:mainfrom
winklemad:winklemad/fix-datetime-negative-truncation
Open

Floor DateTime truncation for pre-epoch timestamps#3002
winklemad wants to merge 3 commits into
quickwit-oss:mainfrom
winklemad:winklemad/fix-datetime-negative-truncation

Conversation

@winklemad

Copy link
Copy Markdown

DateTime::truncate and the into_timestamp_secs/millis/micros helpers used toward-zero integer division (/) where floor division is required. For a pre-1970 (negative) timestamp with a sub-second remainder, / rounds toward zero — i.e. forward in time — so an instant is truncated into the following bucket, and two instants inside the same wall-clock second land in different buckets.

truncate(DATE_TIME_PRECISION_INDEXED) (seconds) builds the indexed term and fast-field value for date fields (from_field_date_for_search, the segment writer, JSON utils, the fast-field writer), so any indexed date before 1970 with a sub-second component is stored under the wrong second and can be missed by exact-match/range date queries. Positive timestamps are unaffected.

Switched to Euclidean (floor) division (div_euclid), so truncation always maps an instant to the start of its containing bucket. Added regression tests in common/src/datetime.rs asserting truncation never moves a timestamp forward and that same-second instants co-bucket (positives unchanged).

Note: a similar toward-zero division exists in the aggregation date_histogram path (a separate code path that divides OffsetDateTime::unix_timestamp_nanos() directly, not via DateTime). I've kept this PR scoped to the DateTime indexing path; happy to address the aggregation case in a follow-up.

DateTime::truncate and the into_timestamp_* helpers relied on plain
integer division, which rounds towards zero. For timestamps before 1970
that rounds a value up in time, so an instant could be truncated into the
following bucket and two instants inside the same second could end up in
different buckets.

truncate() is what builds the indexed term and the fast field value for
date fields (at second precision), so pre-1970 dates with a sub-second
component were stored under the wrong second and could be missed by exact
and range queries.

Use Euclidean division so truncation always floors to the start of the
containing bucket, matching the behaviour already seen for positive
timestamps.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd55312233

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread common/src/datetime.rs Outdated
DateTimePrecision::Microseconds => (self.timestamp_nanos / 1_000) * 1_000,
let truncated_timestamp_nanos = match precision {
DateTimePrecision::Seconds => {
self.timestamp_nanos.div_euclid(1_000_000_000) * 1_000_000_000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard flooring at the DateTime lower bound

When truncating timestamps in the first partial bucket above DateTime::MIN (for example, DateTime::MIN.truncate(DateTimePrecision::Seconds)), the Euclidean quotient is one lower and multiplying it back produces a value below i64::MIN. In debug builds this panics; in typical release builds it wraps to a far-future positive timestamp, so indexing or searching with a lower-bound date can write the wrong term/fast-field value. Please saturate or special-case floor buckets that are outside the representable DateTime range.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eb1624daf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread common/src/datetime.rs Outdated
/// Convert to UNIX timestamp in microseconds.
pub const fn into_timestamp_micros(self) -> i64 {
self.timestamp_nanos / 1_000
self.timestamp_nanos.div_euclid(1_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp lower-bound microsecond conversions

For DateTime values in the first partial microsecond bucket above i64::MIN (for example DateTime::MIN), this floor division returns -9223372036854776, whose corresponding nanosecond value is below i64::MIN. Since BinarySerializable::serialize uses into_timestamp_micros() and deserialization calls from_timestamp_micros(), round-tripping such a valid DateTime now panics in debug builds or wraps to a far-future timestamp in release builds; the previous toward-zero quotient stayed within the constructible range.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant